package com.shutuo.callnummeiwei.net;

import android.text.TextUtils;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

import ashy.earl.common.closure.EarlCall;
import ashy.earl.common.closure.Method2_0;
import ashy.earl.common.closure.Method4_0;
import ashy.earl.common.closure.Params2;
import ashy.earl.common.closure.Params4;
import ashy.earl.common.task.MessageLoop;
import ashy.earl.common.task.Task;
import ashy.earl.common.task.annotation.CreateThread;
import ashy.earl.common.util.IoUtil;
import ashy.earl.common.util.L;

import static ashy.earl.common.closure.Earl.bind;

/**
 * 接受客户端的连接请求，新的连接socket会回到主线程中。
 */
public class ServerSocketAccepter {
    private static final String TAG_END = "accepter";
    public static final int STATE_STOP = 1;
    public static final int STATE_STARTING = 2;
    public static final int STATE_STARTED = 3;
    private static final int RETRY_DELAY = 10000;// 10s
    private final String mTag;
    private final String mCategory;
    private final MessageLoop mCreateLoop;
    private MessageLoop mAccepterLoop;
    @State
    private int mState = STATE_STOP;
    private AcceptListener mAcceptListener;
    private int mSetupPort = -1;
    private volatile Object mToken;
    private Task mRetryAcceptTask;
    private ServerSocket mServerSocket;

    @IntDef({STATE_STOP, STATE_STARTING, STATE_STARTED})
    @Retention(RetentionPolicy.SOURCE)
    public @interface State {
    }

    public interface AcceptListener {
        @CreateThread
        void onNewClientSocket(Socket socket, String ip, int port);

        @CreateThread
        void onStateChanged(@State int state);
    }

    public static String stateToString(@State int state) {
        switch (state) {
            case STATE_STARTED:
                return "started";
            case STATE_STARTING:
                return "starting";
            case STATE_STOP:
                return "stop";
            default:
                return "unknow-" + state;
        }
    }

    @CreateThread
    public ServerSocketAccepter(String category, String name, MessageLoop createLoop) {
        this.mTag = TextUtils.isEmpty(name) ? TAG_END : (name + '-' + TAG_END);
        mCategory = category;
        mCreateLoop = createLoop;
    }

    @CreateThread
    public ServerSocketAccepter(String category, String name) {
        this.mTag = TextUtils.isEmpty(name) ? TAG_END : (name + '-' + TAG_END);
        mCategory = category;
        mCreateLoop = MessageLoop.current();
    }

    @CreateThread
    public void setup(int port, AcceptListener listener) {
        throwIfNotCreateLoop();
        if (mState != STATE_STOP) {
            L.e(mCategory, "%-15s~ setup already setup on port[%d]", mTag, mSetupPort);
            return;
        }
        if (L.loggable(mCategory, L.DEBUG)) {
            L.d(mCategory, "%-15s~ setup, port:%d", mTag, port);
        }
        mAcceptListener = listener;
        mSetupPort = port;
        mAccepterLoop = MessageLoop.prepare(mTag);
        mToken = new Object();
        mAccepterLoop.postTask(bind(setupServerSocket, this, mToken, port).task());
        setState(STATE_STARTING);
    }

    @CreateThread
    public void teardown() {
        throwIfNotCreateLoop();
        if (mState == STATE_STOP) return;
        if (L.loggable(mCategory, L.DEBUG)) {
            L.d(mCategory, "%-15s~ teardown", mTag);
        }
        mToken = null;
        mSetupPort = -1;
        if (mRetryAcceptTask != null) {
            mRetryAcceptTask.cancel();
            mRetryAcceptTask = null;
        }
        mAccepterLoop.quit();
        mAccepterLoop = null;
        IoUtil.closeQuitly(mServerSocket);
        mServerSocket = null;
        setState(STATE_STOP);
    }

    @EarlCall
    private void setupServerSocket(Object token, int port) {
        if (token != mToken) return;
        ServerSocket serverSocket;
        try {
            serverSocket = new ServerSocket(port);
            mCreateLoop.postTask(
                    bind(didSetupServerSocket, this, token, serverSocket, port, null).task());
        } catch (IOException e) {
            mCreateLoop.postTask(bind(didSetupServerSocket, this, token, null, port, e).task());
            return;
        }
        while (token == mToken) {
            try {
                Socket socket = serverSocket.accept();
                InetSocketAddress address = (InetSocketAddress) socket.getRemoteSocketAddress();
                String ip = address.getAddress().getHostAddress();
                int clientPort = address.getPort();
                if (L.loggable(mCategory, L.DEBUG)) {
                    L.d(mCategory, "%-15s~ new client socket: %s:%d", mTag, ip, clientPort);
                }
                mCreateLoop.postTask(
                        bind(didGotNewSocket, this, token, socket, ip, clientPort).task());
            } catch (IOException e) {
                if (L.loggable(mCategory, L.DEBUG)) {
                    L.d(mCategory, "%-15s~ accept client socket error - %s", mTag, e);
                }
            }
        }
    }

    @EarlCall
    private void didGotNewSocket(Object token, Socket socket, String ip, int port) {
        if (token != mToken) {
            IoUtil.closeQuitly(socket);
            return;
        }
        if (mAcceptListener != null) mAcceptListener.onNewClientSocket(socket, ip, port);
    }

    @EarlCall
    private void didSetupServerSocket(Object token, ServerSocket serverSocket, int port, IOException e) {
        if (token != mToken) {
            IoUtil.closeQuitly(serverSocket);
            return;
        }
        mRetryAcceptTask = null;
        if (e != null) {
            if (L.loggable(mCategory, L.DEBUG)) {
                L.d(mCategory, "%-15s~ setup server socket on port[%d] error, retry after %dms : %s", mTag,
                    port, RETRY_DELAY, e);
            }
            setState(STATE_STARTING);
            mRetryAcceptTask = bind(setupServerSocket, this, mToken, mSetupPort).task();
            mAccepterLoop.postTaskDelayed(mRetryAcceptTask, RETRY_DELAY);
            return;
        }
        mServerSocket = serverSocket;
        if (L.loggable(mCategory, L.DEBUG)) {
            L.d(mCategory, "%-15s~ server socket setup on port:%d", mTag, port);
        }
        setState(STATE_STARTED);

    }

    private void setState(@State int state) {
        if (mState == state) return;
        if (L.loggable(mCategory, L.DEBUG)) {
            L.d(mCategory, "%-15s~ setState: %s -> %s", mTag, stateToString(mState), stateToString(state));
        }
        mState = state;
        if (mAcceptListener != null) mAcceptListener.onStateChanged(state);
    }

    @State
    public int getState() {
        return mState;
    }


    private void throwIfNotCreateLoop() {
        if (MessageLoop.current() == mCreateLoop) return;
        throw new IllegalAccessError(
                "Can't access this method on wrong loop, should on " + mCreateLoop +
                ", current is:" + MessageLoop.current());
    }

    private static final Method2_0<ServerSocketAccepter, Void, Object, Integer> setupServerSocket = new Method2_0<ServerSocketAccepter, Void, Object, Integer>(
            ServerSocketAccepter.class, "setupServerSocket") {
        @Override
        public Void run(ServerSocketAccepter target, @NonNull Params2<Object, Integer> params) {
            target.setupServerSocket(params.p1, u(params.p2));
            return null;
        }
    };
    private static final Method4_0<ServerSocketAccepter, Void, Object, Socket, String, Integer> didGotNewSocket = new Method4_0<ServerSocketAccepter, Void, Object, Socket, String, Integer>(
            ServerSocketAccepter.class, "didGotNewSocket") {
        @Override
        public Void run(ServerSocketAccepter target, @NonNull Params4<Object, Socket, String, Integer> params) {
            target.didGotNewSocket(params.p1, params.p2, params.p3, u(params.p4));
            return null;
        }
    };
    private static final Method4_0<ServerSocketAccepter, Void, Object, ServerSocket, Integer, IOException> didSetupServerSocket = new Method4_0<ServerSocketAccepter, Void, Object, ServerSocket, Integer, IOException>(
            ServerSocketAccepter.class, "didSetupServerSocket") {
        @Override
        public Void run(ServerSocketAccepter target, @NonNull Params4<Object, ServerSocket, Integer, IOException> params) {
            target.didSetupServerSocket(params.p1, params.p2, u(params.p3), params.p4);
            return null;
        }
    };
}
